package biolabs.model.abscretion

import biolabs.core.{BioObject, Interaction, WrappedDelegate}
import biolabs.model.PositioningBioContainer
import biolabs.space.ContinuousPosition
import biolabs.space.DiscretePosition
import biolabs.space.GenericGrid
import biolabs.util.Graph

import java.util.LinkedList
import java.util.concurrent.ConcurrentSkipListMap

import scala.collection.JavaConversions._
import scala.math.{floor, min}

class Abscretion[
	T <: BioObject with Abscretor[T, P],
	P <: DiscretePosition[P, C],
	C <: ContinuousPosition[C, P]
]
extends Interaction[
	SubstanceHolder[P, C] with PositioningBioContainer[T, P],
	Unit,
	Unit,
	AbscretionDelegate[P],
	T
] {
	type Parent = SubstanceHolder[P, C] with PositioningBioContainer[T, P]

	override def perform(
			parent: Parent,
			participants: Iterable[WrappedDelegate[AbscretionDelegate[P], T]],
			notifyProgress: Unit=>Unit
	): Unit = {
		if(!participants.isEmpty)
			exchangeSubstance(parent, participants, participants.head.substance);
	}

	private class GraphBuilder(parent: Parent) {
		val ipoints = new GenericGrid[AbscretionPoint, P, C](parent.getDimensions());
		val mysources = new ConcurrentSkipListMap[P, AbscretionPoint]()
		val mytargets = new ConcurrentSkipListMap[P, AbscretionPoint]()
		val g = new Graph[AbscretionPoint, SubstanceRedistribution.EdgeData]()

		def getSources() = mysources
		def getTargets() = mytargets
		def getGraph() = g

		def addInteractionPoint(child: T, pos: P, ap: AbscretionPoint): Unit = {

			if(!parent.isValidPos(pos)) {
				System.err.println("Warning: bio-object " + child + " returned interaction point at " + pos + ", which is out of range. Ignoring.")
				return
			} else if(parent.getChildAt(pos) ne child) {
				System.err.println("Warning: bio-object " + child + " returned interaction point at " + pos + ", which is outside the it's shape. Ignoring.")
				return
			}
			val balance = ap.getAmount() - ap.getDemand()
			if(balance == 0)
				return
			ipoints.setField(pos, ap);
			g.addVertex(ap);
			var ip = new AbscretionPoint();
			for(np <- pos.getNeighbors()) {
				if(parent.isValidPos(np)) {
					if(parent.getChildAt(np) eq child) {
						// do nothing
					} else {
						var nip = ipoints.at(np);
						if(nip != null){
							// We don't mind adding an edge between two sources or two targets.
							// Requires correct handling if we are adding the same edge twice
							// (might happen if run concurrently).
							g.addEdge(ap, new SubstanceRedistribution.EdgeData(), nip);
						} else if(parent.getChildAt(np) == null) {
							val m = if(balance > 0) mytargets else mysources
							nip = m.putIfAbsent(np, ip);
							if(nip == null){
								nip = ip;
								ip = new AbscretionPoint();
							}
							g.addVertex(nip);
							g.addEdge(ap, new SubstanceRedistribution.EdgeData(), nip);
						}
					}
				}
			}
		}
	}

	/**
	 * Performs the interaction on the given substance.
	 * It is not reentrant because
	 * {@link ChildBioObject#getPositionedInteractionPoints(InteractionType)}
	 * is not reentrant by design.
	 */
	private def exchangeSubstance(parent: Parent, participants: Iterable[WrappedDelegate[AbscretionDelegate[P], T]], s: Any): Unit = {

		val gb = new GraphBuilder(parent)

		// TODO could be done concurrently for all children bio-objects,
		// but then concurrent implementation of InteractionGraph
		// and one lock for each position in ipoints would be required
		var i = 0
		for(p <- participants) {
			for((pos, ap) <- p.points) {

				// Compute the sensed amounts
				var sensed = 0.0
				for(n <- pos.getNeighbors) {
					if(parent.isValidPos(n)) {
						val nchild = parent.getChildAt(n)
						if(nchild == null) {
							sensed += parent.getConcentration(s).at(n).doubleValue()
						} else if(nchild ne p.owner) {
							nchild.getAbscretionPoint(s, n) match {
								case Some(nap) =>
									val balance = nap.amount - nap.demand
									// only neighbors with positive balance are taken into account
									if(balance > 0)
										sensed += balance
								case None =>
									// do nothing
							}
						}
					}
				}
				ap.sensed = min(sensed, ap.sensorCapacity);

				gb.addInteractionPoint(p.owner, pos, ap)
			}
		}

		val mysources = gb.getSources()
		val mytargets = gb.getTargets()
		val g = gb.getGraph()

		val sconc = parent.getConcentration(s);

		// set the amount for my sources
		for(pip <- mysources.entrySet()) {
			val c = sconc.at(pip.getKey()).doubleValue()
			val f: Int = floor(c).asInstanceOf[Int]
			sconc.setField(pip.getKey(), c-f);
			pip.getValue().setAmount(f);
		}

		// set the demand for my targets
		for(tgt <- mytargets.values()) {
			tgt.setDemand(Integer.MAX_VALUE);
		}

		// NOTE: from here on the code could be run in parallel for different substances.
		// So this method could be split into two, the latter allowing concurrent execution.

		SubstanceRedistribution.redistribute(g);

		// return the remainder from my sources
		for(src <- mysources.entrySet()) {
			val c = sconc.at(src.getKey()).doubleValue() + src.getValue().getAmount()
			sconc.setField(src.getKey(), c)
		}

		// add the amounts caught by my targets
		for(tgt <- mytargets.entrySet()) {
			val c = sconc.at(tgt.getKey()).doubleValue() + tgt.getValue().getAmount()
			sconc.setField(tgt.getKey(), c)
		}

		// TODO could be done concurrently for all child bio-objects
		for(p <- participants)
			p.commit();
	}
}